Skip to content

Changes ToolBarDropDownButton from Gtk.MenuButton to Gtk.Dropdown#2092

Merged
cameronwhite merged 36 commits intoPintaProject:masterfrom
pedropaulosuzuki:dropdown
Apr 24, 2026
Merged

Changes ToolBarDropDownButton from Gtk.MenuButton to Gtk.Dropdown#2092
cameronwhite merged 36 commits intoPintaProject:masterfrom
pedropaulosuzuki:dropdown

Conversation

@pedropaulosuzuki
Copy link
Copy Markdown
Contributor

@pedropaulosuzuki pedropaulosuzuki commented Apr 3, 2026

This allows us to see which element is currently selected in a ToolbarDropDownButton, and also allows us to ship an icon to each list item when the dropdown is opened.

Fix: #1977

@badcel
Copy link
Copy Markdown
Contributor

badcel commented Apr 4, 2026

I can't tell you why your code is not working, but there is a working Gtk.Dropdown sample over at GirCore.

The sample is probably doing what you want and a bit more as the open list is showing more data than the selected item. The logic is not encapsulated in a single class though but this should only be some shifting around of code.

How it looks: https://github.com/gircore/gir.core/tree/main/src/Samples/Gtk-4.0/DropDown
Code: https://github.com/gircore/gir.core/blob/main/src/Samples/Gtk-4.0/DropDown/WithListStore.cs

@pedropaulosuzuki pedropaulosuzuki marked this pull request as ready for review April 4, 2026 20:20
@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

I can't tell you why your code is not working, but there is a working Gtk.Dropdown sample over at GirCore.

The sample is probably doing what you want and a bit more as the open list is showing more data than the selected item. The logic is not encapsulated in a single class though but this should only be some shifting around of code.

How it looks: https://github.com/gircore/gir.core/tree/main/src/Samples/Gtk-4.0/DropDown Code: https://github.com/gircore/gir.core/blob/main/src/Samples/Gtk-4.0/DropDown/WithListStore.cs

Thanks! I was able to fix it now!

Unfortunately the example uses the same "GetSibling" tricks from dev.to's resources, so it didn't help much in the end. I baked the widgets in the ListItem instead and accessed them through the Selected index, which prevented a lot of nulls/warnings.

@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

@cameronwhite I think I fixed everything, but wouldn't hurt to see if I didn't let anything slip. The problems I faced earlier can be seen on the edit history of the Merge Request message, if needed.

@badcel
Copy link
Copy Markdown
Contributor

badcel commented Apr 4, 2026

Ah yes @kashifsoofi is the author of both.

What is wrong about calling next sibling? At least from my point of view the code from @kashifsoofi is less complex as it only needs the liststore as a member and the StringHolder class as a data container which is not stored as a member.

@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

pedropaulosuzuki commented Apr 4, 2026

Ah yes @kashifsoofi is the author of both.

It surely helped to get starting, so I'm very thankful for the article!

What is wrong about calling next sibling? At least from my point of view the code from @kashifsoofi is less complex as it only needs the liststore as a member and the StringHolder class as a data container which is not stored as a member.

Well, every time the binding was updated, it required to walk the widget tree to find the correct elements and account for possible nulls, which gets annoying really fast. Just storing the widgets and querying the position was much simpler and avoided a lot of boilerplate code. Also, it avoids OnSetupListItem completely and avoids most of OnSetupSelectedItem, which is a big win in my book. Since we're initializing everything anyway, better to do it on AddItem than to have to do it in two steps and then lose track of things in the middle of the way. I also had problems being able to create the correct GType for the ListStore (skill issue on my part, maybe?), so I tried something that was less obscure.

EDIT: wasn't skill issue, new Gio.ListStore (ToolBarItem.GetGType ()); was broken, while Gio.ListStore.New (ToolBarItem.GetGType ()); works, which was unexpected. As I was using the new () syntax, it didn't work, no matter what I did.

Also the 'selected' bind was completely broken for some reason, so as I had to do things manually anyway, it was better to just move everything to logic I control and understand instead of depending on somewhat broken/unintuitive GTK behavior.

@badcel
Copy link
Copy Markdown
Contributor

badcel commented Apr 6, 2026

In regard to the Gio.ListStore I'm going to add a new generic overload which should make it a little easier to create the desired ListStore.

@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

In regard to the Gio.ListStore I'm going to add a new generic overload which should make it a little easier to create the desired ListStore.

Thanks! That'll come in handy!

@cameronwhite
Copy link
Copy Markdown
Member

cameronwhite commented Apr 7, 2026

From a quick review, I agree with the desire to avoid GetNextSibling() which is brittle / duplicate code that's difficult to maintain if the UI layout is adjusted. But I don't think working around it with extra sidecar data is the best approach either :)

IMO a good way to do this is to add a custom widget type for the list items. This makes the update simple since the widget can store references to the updatable child widgets it needs. The GTK list model should be storing the data items (ToolBarItem, or something similar to it, in this case).

This would end up looking pretty similar to existing code like the history or layer widgets:

public void Update (HistoryListViewItem item)

@badcel
Copy link
Copy Markdown
Contributor

badcel commented Apr 7, 2026

That sounds good. Perhaps we should update the GirCore sample accordingly to have something users can reuse more easily (including having a dedicated class for the dropdown)?

@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

pedropaulosuzuki commented Apr 7, 2026

In regard to the Gio.ListStore I'm going to add a new generic overload which should make it a little easier to create the desired ListStore.

Btw: I found out just now why my code didn't work before:

new Gio.ListStore (ToolBarItem.GetGType ()); // (1) Doesn't work
Gio.ListStore.New (ToolBarItem.GetGType ()); // (2) Works

As I was using (1), it just didn't work, no matter what I did. @badcel, would it be possible to allow (1) to work the same way as (2)? I imagine many others would face the same problem as I did.

I imagine we would just need to add a constructor that called Gio.ListStore.New (GType) under the hood, no?

@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

From a quick review, I agree with the desire to avoid GetNextSibling() which is brittle / duplicate code that's difficult to maintain if the UI layout is adjusted. But I don't think working around it with extra sidecar data is the best approach either :)

IMO a good way to do this is to add a custom widget type for the list items. This makes the update simple since the widget can store references to the updatable child widgets it needs. The GTK list model should be storing the data items (ToolBarItem, or something similar to it, in this case).

This would end up looking pretty similar to existing code like the history or layer widgets:

public void Update (HistoryListViewItem item)

@cameronwhite Done! I made ToolBarItem a Gtk.Box, which gets passed to the Gio.ListStore. Believe we should be all good now!

@badcel
Copy link
Copy Markdown
Contributor

badcel commented Apr 10, 2026

I checked the GTK Dropdown code how they do it. As they notify like in the GirCore example I think the code is good enough for me. The additional classes make it a little more verbose but things are more structured at the same time.

@cameronwhite
Copy link
Copy Markdown
Member

Perhaps that GtkListItem::selected property is the issue here and it has some other meaning for events like hover.
That GTK code for the default string menu implementation is only listening to GtkDropDown::selected-item so we should try just doing that.

@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

pedropaulosuzuki commented Apr 11, 2026

Perhaps that GtkListItem::selected property is the issue here and it has some other meaning for events like hover. That GTK code for the default string menu implementation is only listening to GtkDropDown::selected-item so we should try just doing that.

Agreed, it looks like selected here just means focused.

At the moment I'm updating on OnBindSelectedItem, which checks if the selection changed and does the switch. If we listened to GtkDropDown::selected-item, it would do the same, just would be more code, more callbacks, and so on.

Edit: Here's what Matthias Clasen says about this situation:

image

Edit 2: Also some feedback from Alice Mikhaylenko:

image

So I guess our current implementation is the way to go.

Comment thread Pinta.Core/Widgets/ToolBarDropDownButton.cs Outdated
Comment thread Pinta.Core/Widgets/ToolBarDropDownButton.cs Outdated
Comment thread Pinta.Core/Widgets/ToolBarDropDownButton.cs Outdated
{
Gtk.ListItem item = (Gtk.ListItem) args.Object;
if (item is null) { return; }
item.SetChild (selected_box);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm uneasy about doing non-idiomatic GTK things here like reusing a separate widget.
Could this just create a ToolBarItemWidget? It's basically the same as the list widgets, except you'd need the option for whether to show the label.
A method like ToolBarItemWidget.Bind(toolbarItem, showLabel, showSelected) could work

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, it is not a ToolbarItemWidget. It is another widget, where there is no "selected icon", there is, in some cases, no label, and there is only an icon. If we hide those items in one place, they will be hidden everywhere, so we cannot reuse them. So we would need to create another widget just for that, which is not worth the code complexity. It's all local anyway, so this wouldn't be exposed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we hide those items in one place, they will be hidden everywhere

I'm not really understanding what you mean here?
My suggestion was that you'd create a separate instance of a ToolbarItemWidget in OnSetupSelectedItem (different than the ones created in OnSetupListItem). So that widget instance can have its options configured appropriately for showing the checkmark / label, and it's a bit more organized than maintaining references to several separate widgets

Copy link
Copy Markdown
Contributor Author

@pedropaulosuzuki pedropaulosuzuki Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we hide those items in one place, they will be hidden everywhere

I'm not really understanding what you mean here? My suggestion was that you'd create a separate instance of a ToolbarItemWidget in OnSetupSelectedItem (different than the ones created in OnSetupListItem). So that widget instance can have its options configured appropriately for showing the checkmark / label, and it's a bit more organized than maintaining references to several separate widgets

Maybe, but the ToolBarItemWidget has immutable text/icon, we'd need to make it mutable (is it possible, but could conflict with the ToolBarItemWidgets, since the SelectedItem is a single mutable widget and the rest are a bunch of immutable ones). That's why either creating a specific widget for that or doing the widgets as private fields might be a better option.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any objections to making the widget mutable - that's actually the intended approach for GTK list views. (e.g. if you have a long list where not all items are visible, the list view might only create the number of widgets required for visible items, and then as you scroll it's just rebinding those widgets to different items in the model - this is the reason for the separate "setup" and "bind" methods in the factory.

But I don't feel too strongly either way since it's conceptually a different type of widget from the list items - it just happens to be similar enough that you could make use of it.

Copy link
Copy Markdown
Contributor Author

@pedropaulosuzuki pedropaulosuzuki Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any objections to making the widget mutable - that's actually the intended approach for GTK list views. (e.g. if you have a long list where not all items are visible, the list view might only create the number of widgets required for visible items, and then as you scroll it's just rebinding those widgets to different items in the model - this is the reason for the separate "setup" and "bind" methods in the factory.

I think the idea of having the SelectedItem as a model is for added flexibility, I can think of specific cases where you want to show a different widget depending on which item is selected, maybe with additional buttons and actions for some fancy programs. However, for the general case, you probably want an immutable widget where you just use methods to change the text or an icon. Or, as Alice said, most people don't even go there and just avoid custom factories and use the dropdown as-is. You would never create/show more than one of those widgets at the same time tho, so it's a bit disjoint from the usual ListView logic.

But I don't feel too strongly either way since it's conceptually a different type of widget from the list items - it just happens to be similar enough that you could make use of it.

Yeah, it's one of those cases of "we can, but maybe we shouldn't". For example, we would never want a checked item in the SelectedItem, so it would be always there, hidden in the widget tree, taking up memory for something that will never come true. Not that it matters that much, but I think those widgets have different expectations.

toolbar_item_widgets = [];
show_label = showLabel;

widgetList = Gio.ListStore.New (ToolBarItemWidget.GetGType ());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels very weird having the "model" list storing the widgets. This should really be a list of the ToolBarItem's

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we did that, then we'd need to make ToolBarItem inherit from GObject, which I don't see the benefit. That's why I used an empty string list at the start, because we only need the indexes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I think the benefit is that the code is less hacky :) Having a model-view widget, but using the widget also as the model, is very odd and goes against the intended use of these GTK apis where widgets are created on demand for the model.

Since the BaseTool implementations interact with ToolBarItem there is some justification for not wanting it to introduce a dependency on GObject, although the better change in the long run would be to isolate the tools from this widget entirely and have something like https://github.com/PintaProject/Pinta/blob/master/Pinta.Core/Classes/ToolOption/IntegerOption.cs for these enum-style options to remove the UI dependency.

With that reasoning I could be okay with leaving this as-is, but it would need a comment explaining the unusual choice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I think the benefit is that the code is less hacky :) Having a model-view widget, but using the widget also as the model, is very odd and goes against the intended use of these GTK apis where widgets are created on demand for the model.

Since the BaseTool implementations interact with ToolBarItem there is some justification for not wanting it to introduce a dependency on GObject, although the better change in the long run would be to isolate the tools from this widget entirely and have something like https://github.com/PintaProject/Pinta/blob/master/Pinta.Core/Classes/ToolOption/IntegerOption.cs for these enum-style options to remove the UI dependency.

With that reasoning I could be okay with leaving this as-is, but it would need a comment explaining the unusual choice.

I believe you're right, it can be detrimental to have a widget there. Maybe the StringList option is better (with the comment of avoiding depending on GObject), as I don't use those widgets in the model anyway, and that would make this more explicit.

I like the idea of isolating the tools a lot! Hopefully that can eliminate some of these workarounds. If tools don't depend on the DropDown to retrieve state, then we don't need to store this on the widgets.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted to StringList to avoid having widgets on the ListStore and to not make ToolBarItem inherit from GObject.

Comment thread Pinta.Core/Widgets/ToolBarDropDownButton.cs Outdated
Comment thread Pinta.Core/Widgets/ToolBarDropDownButton.cs Outdated
@pedropaulosuzuki
Copy link
Copy Markdown
Contributor Author

@cameronwhite my patch doesn't address disabling items in the dropdown yet, which could be useful in a few instances (like #1965), maybe we could take an ToolBarItem and have an 'enabled' property, which we could use to filter changes to the selected item, for example on the if (previous_index != current_index) path. Do you think we try to do this on this PR, or wait for a future one?

@cameronwhite
Copy link
Copy Markdown
Member

I think that's best left for another PR to keep this easier to review - will try to have a look later this week!

pedropaulosuzuki and others added 7 commits April 21, 2026 12:33
This was redundant because 'previous_index' is always initialized at zero. When the selection changes, the SetSelectedIndex function already removes the checkmark on the first item.
Zero-indexed list goes from '0' to 'length - 1'.
@cameronwhite
Copy link
Copy Markdown
Member

Thanks, this is much easier to follow 👍
I just removed a couple redundant null checks that my IDE pointed out, but otherwise looks ok. Thanks for the many iterations on this :)

@cameronwhite cameronwhite merged commit 9abd742 into PintaProject:master Apr 24, 2026
6 checks passed
@pedropaulosuzuki pedropaulosuzuki deleted the dropdown branch April 25, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[GUI] ToolBarDropDownButton doesn't inform currently selected ToolBarItem when opened

3 participants